﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NHibernate.Linq;
using Xenta.Api;
using Xenta.Entities;
using Xenta.Enums;
using Xenta.Plugins;
using Xenta.Security;
using Xenta.Structs;
using Xenta.Utils;

namespace Xenta.Operations
{
    #region __General__

    /// <summary>
    /// Creates a file.
    /// </summary>
    public class CreateFile : CreateAuditable<FileEntity>,
        ISecuredOperation, IApiOperation
    {
        #region Properties

        /// <summary>
        /// Gets the collection of security policies.
        /// </summary>
        public IEnumerable<SecurityPolicy> SecurityPolicies
        {
            get
            {
                yield return new SecurityPolicy()
                    .IsAuthenticated()
                    .HasPermission("FILESYS");
                yield return new SecurityPolicy()
                    .IsAuthenticated()
                    .IsVerified()
                    .Rule((ctx, uow) =>
                    {
                        var owner = ctx.FileOwner(uow);
                        return owner != null &&
                            owner.Flags.HasFlag(FileOwnerFlags.Active) &&
                            owner.EntityID == OwnerID;
                    });
            }
        }

        /// <summary>
        /// Gets or sets the owner identifier.
        /// </summary>
        public int OwnerID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the folder identifier.
        /// </summary>
        public int FolderID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the storage global unique identifier.
        /// </summary>
        public Guid StorageID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the file name.
        /// </summary>
        public string Name
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the MIME type.
        /// </summary>
        public string MimeType
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the file size.
        /// </summary>
        public long Size
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the flags.
        /// </summary>
        public FileFlags Flags
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override Err Validate(IUnitOfWork uow)
        {
            if(String.IsNullOrWhiteSpace(Name))
                return Err.Of<FileEntity>().NotValid("Name");
            if(String.IsNullOrWhiteSpace(MimeType))
                return Err.Of<FileEntity>().NotValid("MimeType");
            if(Size < 0)
                return Err.Of<FileEntity>().NotValid("Size");
            if(!uow.Infrastructure.CheckDependency<IFileStorage>(StorageID))
                return Err.Of<IFileStorage>().NotFound();

            var maxFileSize = new Dictionary<String, String>()
                .Load(uow.Execute(new GetConfigParamValueByKey
                {
                    Key = Par.In("Core").Of<FileEntity>().Name("MaxSize")
                }).Result);
            if(maxFileSize.Keys.Where(x => MimeType.IsMatch(x))
                .Any(x => Size > maxFileSize[x].ParseAs<Int64>()))
            {
                return Err.Of<FileEntity>().NotValid("Size");
            }

            return base.Validate(uow);
        }

        protected override void Construct(IUnitOfWork uow, FileEntity entity)
        {
            base.Construct(uow, entity);

            entity.Guid = Guid.NewGuid();
            entity.Owner = uow.Execute(new GetFileOwner
            {
                ThrowsException = true,
                EntityID = OwnerID
            }).Result;
            entity.Folder = FolderID != 0
                ? uow.Execute(new GetFileFolder
                {
                    ThrowsException = true,
                    EntityID = FolderID
                }).Result
                : null;
            entity.StorageID = StorageID;
            entity.Name = Name.TrimToLen(100);
            entity.MimeType = MimeType.TrimToLen(100);
            entity.Size = Size;
            entity.Flags = Flags;
        }

        protected override void Work(IUnitOfWork uow, out FileEntity result)
        {
            if(uow.Execute(new GetFileOwner
            {
                ThrowsException = true,
                EntityID = OwnerID
            }).Result.QuotaRemainder < Size)
            {
                throw Err.Of<FileOwnerEntity>().ViolateOf("Quota").Ex();
            }

            base.Work(uow, out result);
            using(var ioc = uow.Infrastructure.DependencyResolver())
                (ioc.Resolve(StorageID) as IFileStorage).Create(result);

            uow.Execute(new UpdateFileOwnerStats
            {
                EntityID = result.Owner.EntityID
            });

            if(result.Folder != null)
            {
                uow.Execute(new UpdateFileFolderStats
                {
                    EntityID = result.Folder.EntityID
                });
            }
        }

        #endregion
    }

    /// <summary>
    /// Updates the file.
    /// </summary>
    public class UpdateFile : UpdateAuditable<FileEntity>,
        ISecuredOperation, IApiOperation
    {
        #region Properties

        /// <summary>
        /// Gets the collection of security policies.
        /// </summary>
        public IEnumerable<SecurityPolicy> SecurityPolicies
        {
            get
            {
                yield return new SecurityPolicy()
                    .IsAuthenticated()
                    .HasPermission("FILESYS");
                yield return new SecurityPolicy()
                    .IsAuthenticated()
                    .IsVerified()
                    .Rule((ctx, uow) =>
                    {
                        var owner = ctx.FileOwner(uow);
                        if(owner == null || owner.EntityID != OwnerID)
                            return false;
                        var file = uow.Execute(new GetFile
                        {
                            EntityID = EntityID
                        }).Result;
                        if(file == null)
                            return false;
                        return owner.Flags.HasFlag(FileOwnerFlags.Active) &&
                            file.Owner.EntityID == OwnerID;
                    });
            }
        }

        /// <summary>
        /// Gets or sets the owner identifier.
        /// </summary>
        public int OwnerID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the folder identifier.
        /// </summary>
        public int FolderID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the file name.
        /// </summary>
        public string Name
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the flags.
        /// </summary>
        public FileFlags Flags
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override Err Validate(IUnitOfWork uow)
        {
            if(String.IsNullOrWhiteSpace(Name))
                return Err.Of<FileEntity>().NotValid("Name");
            return base.Validate(uow);
        }

        protected override void Work(IUnitOfWork uow, out FileEntity result)
        {
            var file = uow.Execute(new GetFile
            {
                ThrowsException = true,
                EntityID = EntityID
            }).Result;
            var oldFolder = file.Folder;
            var oldOwner = file.Owner;

            base.Work(uow, out result);

            uow.Execute(new UpdateFileOwnerStats
            {
                EntityID = result.Owner.EntityID
            });

            if(!oldOwner.Equals(result.Owner))
            {
                uow.Execute(new UpdateFileOwnerStats
                {
                    EntityID = oldOwner.EntityID
                });
            }
            if(result.Folder != null)
            {
                uow.Execute(new UpdateFileFolderStats
                {
                    EntityID = result.Folder.EntityID
                });
            }
            if(oldFolder != null && !oldFolder.Equals(result.Folder))
            {
                uow.Execute(new UpdateFileFolderStats
                {
                    EntityID = oldFolder.EntityID
                });
            }
        }

        protected override void Revise(IUnitOfWork uow, FileEntity entity)
        {
            base.Revise(uow, entity);

            entity.Owner = uow.Execute(new GetFileOwner
            {
                ThrowsException = true,
                EntityID = OwnerID
            }).Result;
            entity.Folder = FolderID == 0 ? null : uow.Execute(new GetFileFolder
            {
                ThrowsException = true,
                EntityID = FolderID
            }).Result;
            entity.Name = Name.TrimToLen(100);
            entity.Flags = Flags;
        }

        #endregion
    }

    /// <summary>
    /// Sets the file size.
    /// </summary>
    public class SetFileSize : UpdateAuditable<FileEntity>
    {
        #region Properties

        /// <summary>
        /// Gets or sets the file size.
        /// </summary>
        public long Size
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override Err Validate(IUnitOfWork uow)
        {
            if(Size < 0)
                return Err.Of<FileEntity>().NotValid("Size");
            return base.Validate(uow);
        }

        protected override void Work(IUnitOfWork uow, out FileEntity result)
        {
            base.Work(uow, out result);
            uow.Execute(new UpdateFileOwnerStats
            {
                EntityID = result.Owner.EntityID
            });
            if(result.Folder != null)
            {
                uow.Execute(new UpdateFileFolderStats
                {
                    EntityID = result.Folder.EntityID
                });
            }
        }

        protected override void Revise(IUnitOfWork uow, FileEntity entity)
        {
            base.Revise(uow, entity);

            entity.Size = Size;
        }

        #endregion
    }

    /// <summary>
    /// Deletes the file.
    /// </summary>
    public class DeleteFile : DeleteEntity<FileEntity>,
        ISecuredOperation, IApiOperation
    {
        #region Properties

        /// <summary>
        /// Gets the collection of security policies.
        /// </summary>
        public IEnumerable<SecurityPolicy> SecurityPolicies
        {
            get
            {
                yield return new SecurityPolicy()
                    .IsAuthenticated()
                    .HasPermission("FILESYS");
                yield return new SecurityPolicy()
                    .IsAuthenticated()
                    .IsVerified()
                    .Rule((ctx, uow) =>
                    {
                        var owner = ctx.FileOwner(uow);
                        if(owner == null)
                            return false;
                        var file = uow.Execute(new GetFile
                        {
                            EntityID = EntityID
                        }).Result;
                        if(file == null)
                            return false;
                        return file.Owner.EntityID == owner.EntityID;
                    });
            }
        }

        #endregion

        #region Utilities

        protected override void Work(IUnitOfWork uow)
        {
            var file = uow.Execute(new GetFile
            {
                ThrowsException = true,
                EntityID = EntityID
            }).Result;
            var folder = file.Folder;
            var owner = file.Owner;

            using(var ioc = uow.Infrastructure.DependencyResolver())
                (ioc.Resolve(file.StorageID) as IFileStorage).Delete(file);
            base.Work(uow);

            uow.Execute(new UpdateFileOwnerStats
            {
                EntityID = owner.EntityID
            });
            if(folder != null)
            {
                uow.Execute(new UpdateFileFolderStats
                {
                    EntityID = folder.EntityID
                });
            }
        }

        protected override void Destruct(IUnitOfWork uow, FileEntity entity)
        {
            foreach(var i in entity.Groups.ToList())
            {
                uow.Execute(new UnmapFileFromGroup
                {
                    ChildID = entity.EntityID,
                    EntityID = i.EntityID
                });
            }
            base.Destruct(uow, entity);
        }

        #endregion
    }

    /// <summary>
    /// Gets the file by identifier.
    /// </summary>
    public class GetFile : QueryEntityByID<FileEntity>,
        IApiOperation
    {
        #region Utilities

        protected override IQueryable<FileEntity> Build(IUnitOfWork uow, IQueryable<FileEntity> query)
        {
            query = base.Build(uow, query);
            if(Fetch > FetchLevel.None)
                query = query.Fetch(x => x.Owner)
                    .ThenFetch(x => x.Account);
            return query;
        }

        #endregion
    }

    /// <summary>
    /// Gets the file by global unique identifier.
    /// </summary>
    public class GetFileByGuid : QueryEntity<FileEntity>,
        IApiOperation
    {
        #region Properties

        /// <summary>
        /// Gets or sets the account identifier.
        /// </summary>
        public Guid Guid
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override IQueryable<FileEntity> Build(IUnitOfWork uow, IQueryable<FileEntity> query)
        {
            query = base.Build(uow, query);
            query = query.Where(x => x.Guid == Guid);
            if(Fetch > FetchLevel.None)
                query = query.Fetch(x => x.Owner)
                    .ThenFetch(x => x.Account);
            return query;
        }

        #endregion
    }

    #endregion

    #region __Search__

    /// <summary>
    /// Search over file collection.
    /// </summary>
    public class SearchFiles : SearchOperation<FileEntity>,
        IApiOperation
    {
        #region Ctors

        /// <summary>
        /// Initializes a new class instance.
        /// </summary>
        public SearchFiles()
        {
            Term = SearchTerm.Empty;
            MimeType = SearchTerm.Empty;
            SortDir = SortDirection.Ascending;
            SortBy = new []
            {
                FileSortBy.ID
            };
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the search term.
        /// </summary>
        public SearchTerm Term
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the storage identifier.
        /// </summary>
        public Guid? StorageID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the owner identifier.
        /// </summary>
        public int? OwnerID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the folder identifier.
        /// </summary>
        public int? FolderID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the group identifier.
        /// </summary>
        public int? GroupID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the mime type.
        /// </summary>
        public SearchTerm MimeType
        {
            get;
            set;
        }

        /// <summary>
        ///   Gets or sets the creation date 'from'.
        /// </summary>
        public DateTime? CreatedFrom
        {
            get;
            set;
        }

        /// <summary>
        ///   Gets or sets the creation date 'to'.
        /// </summary>
        public DateTime? CreatedTo
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the flag mask.
        /// </summary>
        public FileFlags FlagMask
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the sort direction.
        /// </summary>
        public SortDirection SortDir
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the sort by.
        /// </summary>
        public FileSortBy[] SortBy
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override IQueryable<FileEntity> Build(IUnitOfWork uow, IQueryable<FileEntity> query)
        {
            query = base.Build(uow, query);
            query = query.Fetch(x => x.Owner).ThenFetch(x => x.Account);
            query = query.FilterByFlags(x => (int)x.Flags, (int)FlagMask);
            query = query.FilterByDateRange(x => x.CreatedOn, CreatedFrom, CreatedTo);
            switch(Term.Mode)
            {
                case SearchTerm.SearchMode.StartsWith:
                    query = query.Where(x => x.Name.StartsWith(Term.Keyword));
                    break;
                case SearchTerm.SearchMode.EndsWith:
                    query = query.Where(x => x.Name.EndsWith(Term.Keyword));
                    break;
                case SearchTerm.SearchMode.Contains:
                    query = query.Where(x => x.Name.Contains(Term.Keyword));
                    break;
                case SearchTerm.SearchMode.Equals:
                    query = query.Where(x => x.Name.Equals(Term.Keyword));
                    break;
            }
            switch(MimeType.Mode)
            {
                case SearchTerm.SearchMode.StartsWith:
                    query = query.Where(x => x.MimeType.StartsWith(MimeType.Keyword));
                    break;
                case SearchTerm.SearchMode.EndsWith:
                    query = query.Where(x => x.MimeType.EndsWith(MimeType.Keyword));
                    break;
                case SearchTerm.SearchMode.Contains:
                    query = query.Where(x => x.MimeType.Contains(MimeType.Keyword));
                    break;
                case SearchTerm.SearchMode.Equals:
                    query = query.Where(x => x.MimeType.Equals(MimeType.Keyword));
                    break;
            }
            if(FolderID.HasValue)
                query = query.Where(x => x.FolderID == FolderID.Value);
            if(OwnerID.HasValue)
                query = query.Where(x => x.Owner.EntityID == OwnerID.Value);
            if(StorageID.HasValue)
                query = query.Where(x => x.StorageID == StorageID.Value);
            if(GroupID.HasValue)
            {
                query = GroupID.Value != 0
                    ? query.Where(x => x.Groups.Any(i => i.EntityID == GroupID.Value))
                    : query.Where(x => !x.Groups.Any());
            }
            if(SortBy != null)
            {
                for(int i = 0; i < SortBy.Length; i++)
                {
                    switch(SortBy[i])
                    {
                        case FileSortBy.Name:
                            query = query.OrderBy(x => x.Name, SortDir, i != 0);
                            break;
                        case FileSortBy.MimeType:
                            query = query.OrderBy(x => x.MimeType, SortDir, i != 0);
                            break;
                        case FileSortBy.Size:
                            query = query.OrderBy(x => x.Size, SortDir, i != 0);
                            break;
                        case FileSortBy.CreatedOn:
                            query = query.OrderBy(x => x.CreatedOn, SortDir, i != 0);
                            break;
                        default:
                            query = query.OrderBy(x => x.EntityID, SortDir, i != 0);
                            break;
                    }
                }
            }
            return query;
        }

        #endregion
    }

    /// <summary>
    /// Gets the collection of files as a dictionary, where 
    /// key - file identifier and value - file name.
    /// </summary>
    public class GetFileDictionary : SearchOperation<FileEntity, Int32, String>,
        IApiOperation
    {
        #region Ctors

        /// <summary>
        /// Initializes a new class instance.
        /// </summary>
        public GetFileDictionary()
        {
            Term = SearchTerm.Empty;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the search term.
        /// </summary>
        public SearchTerm Term
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the flag mask.
        /// </summary>
        public FileFlags FlagMask
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override IQueryable<FileEntity> Build(IUnitOfWork uow, IQueryable<FileEntity> query)
        {
            query = base.Build(uow, query);
            query = query.FilterByFlags(x => (int)x.Flags, (int)FlagMask);
            switch(Term.Mode)
            {
                case SearchTerm.SearchMode.StartsWith:
                    query = query.Where(x => x.Name.StartsWith(Term.Keyword));
                    break;
                case SearchTerm.SearchMode.EndsWith:
                    query = query.Where(x => x.Name.EndsWith(Term.Keyword));
                    break;
                case SearchTerm.SearchMode.Contains:
                    query = query.Where(x => x.Name.Contains(Term.Keyword));
                    break;
                case SearchTerm.SearchMode.Equals:
                    query = query.Where(x => x.Name.Equals(Term.Keyword));
                    break;
            }
            query = query.OrderBy(x => x.Name);
            return query;
        }

        protected override IQueryable<KeyValuePair<Int32, String>> Map(IQueryable<FileEntity> query)
        {
            return query.Select(x => new KeyValuePair<Int32, String>(x.EntityID, x.Name));
        }

        #endregion
    }

    #endregion

    #region __Read/Write__

    /// <summary>
    /// Writes the file.
    /// </summary>
    public class WriteFile : Operation<Int64>,
        ISecuredOperation
    {
        #region Properties

        /// <summary>
        /// Gets the collection of security policies.
        /// </summary>
        public IEnumerable<SecurityPolicy> SecurityPolicies
        {
            get
            {
                yield return new SecurityPolicy()
                    .IsAuthenticated()
                    .HasPermission("FILESYS");
                yield return new SecurityPolicy()
                    .IsAuthenticated()
                    .IsVerified()
                    .Rule((ctx, uow) =>
                    {
                        var owner = ctx.FileOwner(uow);
                        if(owner == null)
                            return false;
                        var file = uow.Execute(new GetFileByGuid
                        {
                            Guid = Guid
                        }).Result;
                        if(file == null)
                            return false;
                        return file.Owner.EntityID == owner.EntityID;
                    });
            }
        }

        /// <summary>
        /// Gets or sets the file global unique identifier.
        /// </summary>
        public Guid Guid
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the data stream.
        /// </summary>
        public Stream InStream
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override Err Validate(IUnitOfWork uow)
        {
            if(InStream == null)
                return Err.Of(this).NotValid("InStream");
            return base.Validate(uow);
        }

        protected override void Work(IUnitOfWork uow, out long result)
        {
            var file = uow.Execute(new GetFileByGuid
            {
                ThrowsException = true,
                Guid = Guid
            }).Result;
            using(var ioc = uow.Infrastructure.DependencyResolver())
            {
                var storage = ioc.Resolve(file.StorageID)
                    as IFileStorage;
                if(!storage.Exists(file))
                    throw Err.Of<FileEntity>().NotIn("Storage").Ex();
                long bytesWritten = storage.Write(file, InStream);
                if(file.Size != bytesWritten)
                {
                    file = uow.Execute(new SetFileSize
                    {
                        Size = bytesWritten
                    }).Result;
                }
                result = bytesWritten;
            }
        }

        #endregion
    }

    /// <summary>
    /// Reads the file.
    /// </summary>
    public class ReadFile : Operation<Int64>
    {
        #region Properties

        /// <summary>
        /// Gets or sets the file global unique identifier.
        /// </summary>
        public Guid Guid
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the data stream.
        /// </summary>
        public Stream OutStream
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override Err Validate(IUnitOfWork uow)
        {
            if(OutStream == null)
                return Err.Of(this).NotValid("OutStream");
            return base.Validate(uow);
        }

        protected override void Work(IUnitOfWork uow, out long result)
        {
            var file = uow.Execute(new GetFileByGuid
            {
                ThrowsException = true,
                Guid = Guid
            }).Result;
            using(var ioc = uow.Infrastructure.DependencyResolver())
            {
                var storage = ioc.Resolve(file.StorageID)
                    as IFileStorage;
                if(!storage.Exists(file))
                    throw Err.Of<FileEntity>().NotIn("Storage").Ex();
                result = storage.Read(file, OutStream);
            }
        }

        #endregion
    }

    #endregion
}
